/*
 * Copyright (c) 2011-2013 GoPivotal, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package sg.atom.utils.datastructure.tuple;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import org.apache.commons.lang.ObjectUtils;

/**
 * A {@literal Tuple} is an immutable {@link Collection} of objects, each of
 * which can be of an arbitrary type.
 *
 * @author Jon Brisbin
 * @author Stephane Maldini
 */
@SuppressWarnings({"rawtypes"})
public class Tuple implements Iterable, Serializable {

    private static final long serialVersionUID = 8777121214502020842L;
    protected final Object[] entries;
    protected final int size;

    /**
     * Creates a new {@code Tuple} that holds the given {@code values}.
     *
     * @param values The values to hold
     */
    public Tuple(/*@Nonnull*/Collection<Object> values) {
        this.entries = (null != values ? values.toArray() : new Object[0]);
        this.size = entries.length;
    }

    /**
     * Creates a new {@code Tuple} that holds the given {@code values}.
     *
     * @param values The values to hold
     */
    public Tuple(Object... values) {
        this.entries = Arrays.copyOf(values, values.length);
        this.size = values.length;
    }

    /**
     * Create a {@link Tuple1} with the given object.
     *
     * @param t1 The first value in the tuple.
     * @param <T1> The type of the first value.
     * @return The new {@link Tuple1}.
     */
    public static <T1> Tuple1<T1> of(T1 t1) {
        return new Tuple1<T1>(t1);
    }

    /**
     * Create a {@link Tuple2} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @return The new {@link Tuple2}.
     */
    public static <T1, T2> Tuple2<T1, T2> of(T1 t1, T2 t2) {
        return new Tuple2<T1, T2>(t1, t2);
    }

    /**
     * Create a {@link Tuple3} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @return The new {@link Tuple3}.
     */
    public static <T1, T2, T3> Tuple3<T1, T2, T3> of(T1 t1, T2 t2, T3 t3) {
        return new Tuple3<T1, T2, T3>(t1, t2, t3);
    }

    /**
     * Create a {@link Tuple4} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param t4 The fourth value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @param <T4> The type of the fourth value.
     * @return The new {@link Tuple4}.
     */
    public static <T1, T2, T3, T4> Tuple4<T1, T2, T3, T4> of(T1 t1, T2 t2, T3 t3, T4 t4) {
        return new Tuple4<T1, T2, T3, T4>(t1, t2, t3, t4);
    }

    /**
     * Create a {@link Tuple5} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param t4 The fourth value in the tuple.
     * @param t5 The fifth value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @param <T4> The type of the fourth value.
     * @param <T5> The type of the fifth value.
     * @return The new {@link Tuple5}.
     */
    public static <T1, T2, T3, T4, T5> Tuple5<T1, T2, T3, T4, T5> of(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5) {
        return new Tuple5<T1, T2, T3, T4, T5>(t1, t2, t3, t4, t5);
    }

    /**
     * Create a {@link Tuple6} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param t4 The fourth value in the tuple.
     * @param t5 The fifth value in the tuple.
     * @param t6 The sixth value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @param <T4> The type of the fourth value.
     * @param <T5> The type of the fifth value.
     * @param <T6> The type of the sixth value.
     * @return The new {@link Tuple6}.
     */
    public static <T1, T2, T3, T4, T5, T6> Tuple6<T1, T2, T3, T4, T5, T6> of(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6) {
        return new Tuple6<T1, T2, T3, T4, T5, T6>(t1, t2, t3, t4, t5, t6);
    }

    /**
     * Create a {@link Tuple7} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param t4 The fourth value in the tuple.
     * @param t5 The fifth value in the tuple.
     * @param t6 The sixth value in the tuple.
     * @param t7 The seventh value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @param <T4> The type of the fourth value.
     * @param <T5> The type of the fifth value.
     * @param <T6> The type of the sixth value.
     * @param <T7> The type of the seventh value.
     * @return The new {@link Tuple7}.
     */
    public static <T1, T2, T3, T4, T5, T6, T7> Tuple7<T1, T2, T3, T4, T5, T6, T7> of(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7) {
        return new Tuple7<T1, T2, T3, T4, T5, T6, T7>(t1, t2, t3, t4, t5, t6, t7);
    }

    /**
     * Create a {@link Tuple8} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param t4 The fourth value in the tuple.
     * @param t5 The fifth value in the tuple.
     * @param t6 The sixth value in the tuple.
     * @param t7 The seventh value in the tuple.
     * @param t8 The eighth value in the tuple.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @param <T4> The type of the fourth value.
     * @param <T5> The type of the fifth value.
     * @param <T6> The type of the sixth value.
     * @param <T7> The type of the seventh value.
     * @param <T8> The type of the eighth value.
     * @return The new {@link Tuple8}.
     */
    public static <T1, T2, T3, T4, T5, T6, T7, T8> Tuple8<T1, T2, T3, T4, T5, T6, T7, T8> of(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8) {
        return new Tuple8<T1, T2, T3, T4, T5, T6, T7, T8>(t1, t2, t3, t4, t5, t6, t7, t8);
    }

    /**
     * Create a {@link TupleN} with the given objects.
     *
     * @param t1 The first value in the tuple.
     * @param t2 The second value in the tuple.
     * @param t3 The third value in the tuple.
     * @param t4 The fourth value in the tuple.
     * @param t5 The fifth value in the tuple.
     * @param t6 The sixth value in the tuple.
     * @param t7 The seventh value in the tuple.
     * @param t8 The eighth value in the tuple.
     * @param tRest The rest of the values.
     * @param <T1> The type of the first value.
     * @param <T2> The type of the second value.
     * @param <T3> The type of the third value.
     * @param <T4> The type of the fourth value.
     * @param <T5> The type of the fifth value.
     * @param <T6> The type of the sixth value.
     * @param <T7> The type of the seventh value.
     * @param <TRest> The type of the last tuple.
     * @return The new {@link Tuple8}.
     */
    public static <T1, T2, T3, T4, T5, T6, T7, T8, TRest extends Tuple> TupleN<T1, T2, T3, T4, T5, T6, T7, T8, TRest> of(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6, T7 t7, T8 t8, Object... tRest) {
        return new TupleN<T1, T2, T3, T4, T5, T6, T7, T8, TRest>(t1, t2, t3, t4, t5, t6, t7, t8, new Tuple(tRest));
    }

    /**
     * Get the object at the given index.
     *
     * @param index The index of the object to retrieve. Starts at 0.
     * @return The object. Might be {@literal null}.
     */
    /*@Nullablel*/
    public Object get(int index) {
        return (size > 0 && size > index ? entries[index] : null);
    }

    /**
     * Turn this {@literal Tuple} into a plain Object array.
     *
     * @return A new Object array.
     */
    public Object[] toArray() {
        return entries;
    }

    /**
     * Return the number of elements in this {@literal Tuple}.
     *
     * @return The size of this {@literal Tuple}.
     */
    public int size() {
        return size;
    }

    @Override
    /*@Nonnull*/
    public Iterator<?> iterator() {
        return Arrays.asList(entries).iterator();
    }

    @Override
    public int hashCode() {
        if (this.size == 0) {
            return 0;
        } else if (this.size == 1) {
            return ObjectUtils.hashCode(this.entries[0]);
        } else {
            int hashCode = 1;
            for (Object entry : this.entries) {
                hashCode = hashCode ^ ObjectUtils.hashCode(entry);
            }
            return hashCode;
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }

        if (!(o instanceof Tuple)) {
            return false;
        }

        Tuple cast = (Tuple) o;

        if (this.size != cast.size) {
            return false;
        }

        boolean eq = true;

        for (int i = 0; i < this.size; i++) {
            if (null != this.entries[i] && !this.entries[i].equals(cast.entries[i])) {
                return false;
            }
        }

        return eq;
    }
}
